<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
	<meta name="keywords" content="Seata、配置中心、配置管理、Spring配置" />
	<meta name="description" content="本文主要介绍Seata配置管理的核心实现以及和Spring配置的交互过程" />
	<!-- 网页标签标题 -->
	<title>Seata配置管理原理解析</title>
  <link rel="shortcut icon" href="/img/seata_logo_small.jpeg"/>
	<link rel="stylesheet" href="/build/blogDetail.css" />
</head>
<body>
	<div id="root"><div class="blog-detail-page" data-reactroot=""><header class="header-container header-container-normal"><div class="header-body"><a href="/zh-cn/index.html"><img class="logo" src="//img.alicdn.com/tfs/TB1gqL1w4D1gK0jSZFyXXciOVXa-1497-401.png"/></a><div class="search search-normal"><span class="icon-search"></span></div><span class="language-switch language-switch-normal">En</span><div class="header-menu"><img class="header-menu-toggle" src="https://img.alicdn.com/tfs/TB14eEmw7P2gK0jSZPxXXacQpXa-38-32.png"/><ul><li class="menu-item menu-item-normal"><a href="/zh-cn/index.html" target="_self">首页</a></li><li class="menu-item menu-item-normal"><a href="/zh-cn/docs/overview/what-is-seata.html" target="_self">文档</a></li><li class="menu-item menu-item-normal"><a href="/zh-cn/docs/developers/developers_dev.html" target="_self">开发者</a></li><li class="menu-item menu-item-normal menu-item-normal-active"><a href="/zh-cn/blog/index.html" target="_self">博客</a></li><li class="menu-item menu-item-normal"><a href="/zh-cn/community/index.html" target="_self">社区</a></li><li class="menu-item menu-item-normal"><a href="/zh-cn/blog/download.html" target="_self">下载</a></li></ul></div></div></header><section class="blog-content markdown-body"><p>说到Seata中的配置管理，大家可能会想到Seata中适配的各种配置中心，其实今天要说的不是这个，虽然也会简单分析Seata和各配置中心的适配过程，但主要还是讲解Seata配置管理的核心实现</p>
<h1>Server启动流程</h1>
<p>在讲配置中心之前，先简单介绍一下Server端的启动流程，因为这一块就涉及到配置管理的初始化，核心流程如下：</p>
<ol>
<li>程序入口在<code>Server#main</code>方法中</li>
<li>获取port的几种形式：从容器中获取；从命令行获取；默认端口</li>
<li>解析命令行参数：host、port、storeMode等参数，这个过程可能涉及到配置管理的初始化</li>
<li>Metric相关：无关紧要，跳过</li>
<li>NettyServer初始化</li>
<li>核心控制器初始化：Server端的核心，还包括几个定时任务</li>
<li>NettyServer启动</li>
</ol>
<p>代码如下，删除了非核心代码</p>
<pre><code class="language-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> IOException </span>{
    <span class="hljs-comment">// 获取port的几种形式：从容器中获取；从命令行获取；默认端口, use to logback.xml</span>
    <span class="hljs-keyword">int</span> port = PortHelper.getPort(args);
    System.setProperty(ConfigurationKeys.SERVER_PORT, Integer.toString(port));

    <span class="hljs-comment">// 解析启动参数，分容器和非容器两种情况</span>
    ParameterParser parameterParser = <span class="hljs-keyword">new</span> ParameterParser(args);

    <span class="hljs-comment">// Metric相关</span>
    MetricsManager.get().init();

    <span class="hljs-comment">// NettyServer初始化</span>
    NettyRemotingServer nettyRemotingServer = <span class="hljs-keyword">new</span> NettyRemotingServer(workingThreads);

    <span class="hljs-comment">// 核心控制器初始化</span>
    DefaultCoordinator coordinator = <span class="hljs-keyword">new</span> DefaultCoordinator(nettyRemotingServer);
    coordinator.init();
    
    <span class="hljs-comment">// NettyServer启动</span>
    nettyRemotingServer.init();
}
</code></pre>
<p>为社么说<code>步骤3</code>中肯能涉及到配置管理的初始化呢？核心代码如下：</p>
<pre><code class="language-java"><span class="hljs-keyword">if</span> (StringUtils.isBlank(storeMode)) {
    storeMode = ConfigurationFactory.getInstance().getConfig(ConfigurationKeys.STORE_MODE,
        SERVER_DEFAULT_STORE_MODE);
}
</code></pre>
<p>如果在启动参数中没有特别指定<code>storeMode</code>，就会通过<code>ConfigurationFactory</code>相关API去获取该配置，在<code>ConfigurationFactory.getInstance()</code>这行代码中，涉及到两部分内容：<code>ConfigurationFactory</code>静态代码初始化和<code>Configuration</code>初始化。接下来我们重点分析这部分内容</p>
<h1>配置管理初始化</h1>
<h2>ConfigurationFactory初始化</h2>
<p>我们知道在Seata中有两个关键配置文件：一个是<code>registry.conf</code>，这是核心配置文件，必须要有；另一个是<code>file.conf</code>，只有在配置中心是<code>File</code>的情况下才需要用到。<code>ConfigurationFactory</code>静态代码块中，其实主要就是加载<code>registry.conf</code>文件，大概如下：</p>
<p>1、在没有手动配置的情况，默认读取<code>registry.conf</code>文件，封装成一个<code>FileConfiguration</code>对象，核心代码如下：</p>
<pre><code class="language-java">Configuration configuration = <span class="hljs-keyword">new</span> FileConfiguration(seataConfigName,<span class="hljs-keyword">false</span>);
FileConfigFactory.load(<span class="hljs-string">"registry.conf"</span>, <span class="hljs-string">"registry"</span>);
FileConfig fileConfig = EnhancedServiceLoader.load(FileConfig.class, <span class="hljs-string">"CONF"</span>, argsType, args);
</code></pre>
<p>2、配置增强：类似代理模式，获取配置时，在代理对象里面做一些其他处理，下面详细介绍</p>
<pre><code class="language-java">Configuration extConfiguration = EnhancedServiceLoader.load(ExtConfigurationProvider.class).provide(configuration);
</code></pre>
<p>3、将步骤2中的代理对象赋值给<code>CURRENT_FILE_INSTANCE</code>引用，在很多地方都直接用到了<code>CURRENT_FILE_INSTANCE</code>这个静态引用</p>
<pre><code class="language-java">CURRENT_FILE_INSTANCE = extConfiguration == <span class="hljs-keyword">null</span> ? configuration : extConfiguration;
</code></pre>
<p>可以简单的认为：<code>CURRENT_FILE_INSTANCE</code>对应的就是<code>registry.conf</code>里面的内容。我认为<code>registry.conf</code>这个文件名取的不太好，歧义太大，叫做<code>bootstrap.conf</code>是不是更好一些？</p>
<h2>Configuration初始化</h2>
<p><code>Configuration</code>其实就是对应配置中心，Seata目前支持的配置中心很多，几乎主流的配置中心都支持，如：apollo、consul、etcd、nacos、zk、springCloud、本地文件。当使用本地文件作为配置中心的时候，涉及到<code>file.conf</code>的加载，当然这是默认的名字，可以自己配置。到这里，大家也基本上知道了<code>registry.conf</code>和<code>file.conf</code>的关系了。</p>
<p><code>Configuration</code>作为单例放在<code>ConfigurationFactory</code>中，所以<code>Configuration</code>的初始化逻辑也是在<code>ConfigurationFactory</code>中，大概流程如下：
1、先从<code>registry.conf</code>文件中读取<code>config.type</code>属性，默认就是<code>file</code></p>
<pre><code class="language-java">configTypeName = CURRENT_FILE_INSTANCE.getConfig(ConfigurationKeys.FILE_ROOT_CONFIG + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR+ ConfigurationKeys.FILE_ROOT_TYPE);
</code></pre>
<p>2、基于<code>config.type</code>属性值加载配置中心，比如默认是:<code>file</code>，则先从<code>registry.conf</code>文件中读取<code>config.file.name</code>读取本地文件配置中心对应的文件名，然后基于该文件名创建<code>FileConfiguration</code>对象，这样就将<code>file.conf</code>中的配置加载到内存中了。同理，如果配置的是其他配置中心，则会通过SPI初始化其他配置中心。还有一点需要注意的是，在这阶段，如果配置中心是本地文件，则会为其创建代理对象；如果不是本地文件，则通过SPI加载对应的配置中心</p>
<pre><code class="language-java"><span class="hljs-keyword">if</span> (ConfigType.File == configType) {
    String pathDataId = String.join(<span class="hljs-string">"config.file.name"</span>);
    String name = CURRENT_FILE_INSTANCE.getConfig(pathDataId);
    configuration = <span class="hljs-keyword">new</span> FileConfiguration(name);
    <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">// 配置增强 代理</span>
        extConfiguration = EnhancedServiceLoader.load(ExtConfigurationProvider.class).provide(configuration);
    } <span class="hljs-keyword">catch</span> (Exception e) {
        LOGGER.error(<span class="hljs-string">"failed to load extConfiguration:{}"</span>, e.getMessage(), e);
    }
} <span class="hljs-keyword">else</span> {
    configuration = EnhancedServiceLoader
            .load(ConfigurationProvider.class, Objects.requireNonNull(configType).name()).provide();
}
</code></pre>
<p>3、基于步骤2创建的<code>Configuration</code>对象，为其再创建一层代理，这个代理对象有两个作用：一个是本地缓存，不需要每次获取配置的时候都从配置中获取；另一个是监听，当配置发生变更会更新它维护的缓存。如下：</p>
<pre><code class="language-java"><span class="hljs-keyword">if</span> (<span class="hljs-keyword">null</span> != extConfiguration) {
    configurationCache = ConfigurationCache.getInstance().proxy(extConfiguration);
} <span class="hljs-keyword">else</span> {
    configurationCache = ConfigurationCache.getInstance().proxy(configuration);
}
</code></pre>
<p>到这里，配置管理的初始化就完成了。<strong>Seata通过先先加载<code>registry.conf</code>文件获取对应的配置中心信息、注册中心，然后再根据获取到的的对应信息初始化配置中心。在使用本地文件作为配置中心的情况下，默认是加载<code>file.conf</code>文件。然后再为对应的配置中心创建对应的代理对象，使其支持本地缓存和配置监听</strong></p>
<p>整理流程还是比较简单，在这里我要抛出几个问题：</p>
<ol>
<li>什么是配置增强？Seata中的配置增强是怎么做的？</li>
<li>如果使用本地文件作为配置中心，就必须要将配置定义在<code>file.conf</code>文件中。如果是Spring应用，能不能直接将对应的配置项定义在<code>application.yaml</code>中？</li>
<li>在上面说的步骤2中，为什么在使用本地文件配置中心的情况下，要先为<code>Configuration</code>创建对应配置增强代理对象，而其他配置中心不用？</li>
</ol>
<p>这3个问题都是紧密联系的，都和Seata的配置增加相关。下面详细介绍</p>
<h1>配置管理增强</h1>
<p>配置增强，简单来说就是为其创建一个代理对象，在执行目标独对象的目标方法时候，执行代理逻辑，从配置中心的角度来讲，就是在通过配置中心获取对应配置的时候，执行代理逻辑。</p>
<ol>
<li>通过<code>ConfigurationFactory.CURRENT_FILE_INSTANCE.getgetConfig(key)</code>获取配置</li>
<li>加载<code>registry.conf</code>文件创建FileConfiguration对象<code>configuration</code></li>
<li>基于<code>SpringBootConfigurationProvider</code>为<code>configuration</code>创建代理对象<code>configurationProxy</code></li>
<li>从<code>configurationProxy</code>中获取配置中心的连接信息<code>file zk nacos 等</code></li>
<li>基于连接信息创建配中心Configuration对象<code>configuration2</code></li>
<li>基于<code>SpringBootConfigurationProvider</code>为<code>configuration2</code>创建代理对象<code>configurationProxy2</code></li>
<li>执行<code>configurationProxy2</code>的代理逻辑</li>
<li>基于key找到对应的SpringBean</li>
<li>执行SpringBean的getXxx方法</li>
</ol>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0cb93fec40df40ba9e8ab9db06cc9b93~tplv-k3u1fbpfcp-watermark.image" alt=""></p>
<h2>配置增强实现</h2>
<p>上面也简单提到过配置增强，相关代码如下：</p>
<pre><code class="language-java">EnhancedServiceLoader.load(ExtConfigurationProvider.class).provide(configuration);
</code></pre>
<ol>
<li>首先通过SPI机获取一个<code>ExtConfigurationProvider</code>对象，在Seata中默认只有一个实现：<code>SpringBootConfigurationProvider</code></li>
<li>通过<code>ExtConfigurationProvider#provider</code>方法为<code>Configuration</code>创建代理对象</li>
</ol>
<p>核心代码如下:</p>
<pre><code class="language-java"><span class="hljs-function"><span class="hljs-keyword">public</span> Configuration <span class="hljs-title">provide</span><span class="hljs-params">(Configuration originalConfiguration)</span> </span>{
    <span class="hljs-keyword">return</span> (Configuration) Enhancer.create(originalConfiguration.getClass(), <span class="hljs-keyword">new</span> MethodInterceptor() {
        <span class="hljs-meta">@Override</span>
        <span class="hljs-function"><span class="hljs-keyword">public</span> Object <span class="hljs-title">intercept</span><span class="hljs-params">(Object proxy, Method method, Object[] args, MethodProxy methodProxy)</span>
            <span class="hljs-keyword">throws</span> Throwable </span>{
            <span class="hljs-keyword">if</span> (method.getName().startsWith(<span class="hljs-string">"get"</span>) &amp;&amp; args.length &gt; <span class="hljs-number">0</span>) {
                Object result = <span class="hljs-keyword">null</span>;
                String rawDataId = (String) args[<span class="hljs-number">0</span>];
                <span class="hljs-keyword">if</span> (args.length == <span class="hljs-number">1</span>) {
                    result = get(convertDataId(rawDataId));
                } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (args.length == <span class="hljs-number">2</span>) {
                    result = get(convertDataId(rawDataId), args[<span class="hljs-number">1</span>]);
                } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (args.length == <span class="hljs-number">3</span>) {
                    result = get(convertDataId(rawDataId), args[<span class="hljs-number">1</span>], (Long) args[<span class="hljs-number">2</span>]);
                }
                <span class="hljs-keyword">if</span> (result != <span class="hljs-keyword">null</span>) {
                    <span class="hljs-comment">//If the return type is String,need to convert the object to string</span>
                    <span class="hljs-keyword">if</span> (method.getReturnType().equals(String.class)) {
                        <span class="hljs-keyword">return</span> String.valueOf(result);
                    }
                    <span class="hljs-keyword">return</span> result;
                }
            }

            <span class="hljs-keyword">return</span> method.invoke(originalConfiguration, args);
        }
    });
}

<span class="hljs-function"><span class="hljs-keyword">private</span> Object <span class="hljs-title">get</span><span class="hljs-params">(String dataId)</span> <span class="hljs-keyword">throws</span> IllegalAccessException, InstantiationException </span>{
    String propertyPrefix = getPropertyPrefix(dataId);
    String propertySuffix = getPropertySuffix(dataId);
    ApplicationContext applicationContext = (ApplicationContext) ObjectHolder.INSTANCE.getObject(OBJECT_KEY_SPRING_APPLICATION_CONTEXT);
    Class&lt;?&gt; propertyClass = PROPERTY_BEAN_MAP.get(propertyPrefix);
    Object valueObject = <span class="hljs-keyword">null</span>;
    <span class="hljs-keyword">if</span> (propertyClass != <span class="hljs-keyword">null</span>) {
        <span class="hljs-keyword">try</span> {
            Object propertyBean = applicationContext.getBean(propertyClass);
            valueObject = getFieldValue(propertyBean, propertySuffix, dataId);
        } <span class="hljs-keyword">catch</span> (NoSuchBeanDefinitionException ignore) {

        }
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> ShouldNeverHappenException(<span class="hljs-string">"PropertyClass for prefix: ["</span> + propertyPrefix + <span class="hljs-string">"] should not be null."</span>);
    }
    <span class="hljs-keyword">if</span> (valueObject == <span class="hljs-keyword">null</span>) {
        valueObject = getFieldValue(propertyClass.newInstance(), propertySuffix, dataId);
    }

    <span class="hljs-keyword">return</span> valueObject;
}
</code></pre>
<p>1、如果方法是以<code>get</code>开头，并且参数个数为1/2/3，则执行其他的获取配置的逻辑，否则执行原生<code>Configuration</code>对象的逻辑
2、我们没必要纠结为啥是这样的规则，这就是Seata的一个约定
3、<code>其他获取配置的逻辑</code>，就是指通过Spring的方式获取对应配置值</p>
<p>到这里已经清楚了配置增强的原理，同时，也可以猜测得出唯一的<code>ExtConfigurationProvider</code>实现<code>SpringBootConfigurationProvider</code>，肯定是和Spring相关</p>
<h2>配置增强与Spring</h2>
<p>在介绍这块内容之前，我们先简单介绍一下Seata的使用方式：</p>
<ol>
<li>非Starter方式：引入依赖 <code>seata-all</code>, 然后手动配置几个核心的Bean</li>
<li>Starter方式： 引入依赖<code>seata-spring-boot-starter</code>，全自动准配，不需要自动注入核心Bean</li>
</ol>
<p><code>SpringBootConfigurationProvider</code>就在<code>seata-spring-boot-starter</code>模块中，也就是说，当我们通过引入<code>seata-all</code>的方式来使用Seata时，配置增强其实没有什么作用，因为此时根本找不到<code>ExtConfigurationProvider</code>实现类，自然就不会增强。</p>
<p>那<code>seata-spring-boot-starter</code>是如何将这些东西串联起来的？</p>
<p>1、首先，在<code>seata-spring-boot-starter</code>模块的<code>resources/META-INF/services</code>目录下，存在一个<code>spring.factories</code>文件，内容分如下</p>
<pre><code>org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.seata.spring.boot.autoconfigure.SeataAutoConfiguration,\

# 暂时不管
io.seata.spring.boot.autoconfigure.HttpAutoConfiguration
</code></pre>
<p>2、在<code>SeataAutoConfiguration</code>文件中，会创建以下Bean： GlobalTransactionScanner 、SeataDataSourceBeanPostProcessor、SeataAutoDataSourceProxyCreator、SpringApplicationContextProvider。前3个和我们本文要讲的内容不相关，主要关注<code>SpringApplicationContextProvider</code>，核心代码非常简单，就是将<code>ApplicationContext</code>保存下来：</p>
<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SpringApplicationContextProvider</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">ApplicationContextAware</span> </span>{
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setApplicationContext</span><span class="hljs-params">(ApplicationContext applicationContext)</span> <span class="hljs-keyword">throws</span> BeansException </span>{
        ObjectHolder.INSTANCE.setObject(OBJECT_KEY_SPRING_APPLICATION_CONTEXT, applicationContext);
    }
}
</code></pre>
<p>3、然后，在<code>SeataAutoConfiguration</code>文件中，还会将一些<code>xxxProperties.Class</code>和对应的Key前缀缓存到<code>PROPERTY_BEAN_MAP</code>中。``xxxProperties<code>就简单理解成</code>application.yaml`中的各种配置项：</p>
<pre><code class="language-java"><span class="hljs-keyword">static</span> {
    PROPERTY_BEAN_MAP.put(SEATA_PREFIX, SeataProperties.class);
    PROPERTY_BEAN_MAP.put(CLIENT_RM_PREFIX, RmProperties.class);
    PROPERTY_BEAN_MAP.put(SHUTDOWN_PREFIX, ShutdownProperties.class);
    ...省略...
}
</code></pre>
<p>至此，整个流程其实已经很清晰，在有<code>SpringBootConfigurationProvider</code>配置增强的时候，我们获取一个配置项的流程如下：</p>
<ol>
<li>先根据<code>p配置项Key</code>获取对应的<code>xxxProperties</code>对象</li>
<li>通过<code>ObjectHolder</code>中的<code>ApplicationContext</code>获取对应<code>xxxProperties</code>的SpringBean</li>
<li>基于<code>xxxProperties</code>的SpringBean获取对应配置的值</li>
<li>至此，通过配置增强，我们成功的获取到<code>application.yaml</code>中的值</li>
</ol>
</section><footer class="footer-container"><div class="footer-body"><img src="//img.alicdn.com/tfs/TB1dGrSwVT7gK0jSZFpXXaTkpXa-4802-1285.png"/><p class="docsite-power">website powered by docsite</p><div class="cols-container"><div class="col col-12"><h3>愿景</h3><p>Seata 是一款阿里巴巴开源的分布式事务解决方案，致力于在微服务架构下提供高性能和简单易用的分布式事务服务。</p></div><div class="col col-6"><dl><dt>文档</dt><dd><a href="/zh-cn/docs/overview/what-is-seata.html" target="_self">Seata 是什么？</a></dd><dd><a href="/zh-cn/docs/user/quickstart.html" target="_self">快速开始</a></dd><dd><a href="https://github.com/seata/seata.github.io/issues/new" target="_self">报告文档问题</a></dd><dd><a href="https://github.com/seata/seata.github.io" target="_self">在Github上编辑此文档</a></dd></dl></div><div class="col col-6"><dl><dt>资源</dt><dd><a href="/zh-cn/blog/index.html" target="_self">博客</a></dd><dd><a href="/zh-cn/community/index.html" target="_self">社区</a></dd></dl></div></div><div class="copyright"><span>Copyright © 2019 Seata</span></div></div></footer></div></div>
	<script src="https://f.alicdn.com/react/15.4.1/react-with-addons.min.js"></script>
	<script src="https://f.alicdn.com/react/15.4.1/react-dom.min.js"></script>
	<script>
		window.rootPath = '';
  </script>
	<script src="/build/blogDetail.js"></script>
	<script>
    var _hmt = _hmt || [];
    (function() {
      var hm = document.createElement("script");
      hm.src = "https://hm.baidu.com/hm.js?104e73ef0c18b416b27abb23757ed8ee";
      var s = document.getElementsByTagName("script")[0];
      s.parentNode.insertBefore(hm, s);
    })();
    </script>
</body>
</html>
